<template>
  <el-form :inline="true" class="demo-form-inline" ref="form">
    <el-row type="flex" justify="center">
      <el-form-item>
        <el-button type="primary" size="mini" @click="onSubmit">生成数独</el-button>
      </el-form-item>
      <el-form-item>
        <el-button type="warning" size="mini" @click="solve" :disabled="!canGetSolve">解答</el-button>
      </el-form-item>
    </el-row>
    <div v-for="(row,i) in maze" :key="i">
      <el-row type="flex" align="middle" justify="center">
        <div v-for="(col,j) in row" :key="j">
          <el-col>
            <el-form-item>
              <input v-model="maze[i][j].pro" type="number" max=9 min=0 style="width: 46px;" :class="judgeColor(i,j)"
                     :disabled="maze[i][j].origin" @change="filledValue(i,j)">
            </el-form-item>
          </el-col>
        </div>
      </el-row>
    </div>
  </el-form>
</template>
<script>
export default {
  name: 'sudoku',
  data() {
    return {
      canGetSolve: true,
      sudoku: [],
      finalSudoku: [],
      maze: [[]],
      nullArray: [],
      allAnswer: [],
      theAnswer: [],
      maybeMap: new Map(),
      answer: [],
      count: 0,
      group: 0
    }
  },
  components: {},
  watch: {},
  computed: {},
  created() {
    this.createSudoku()
  },
  methods: {
    filledValue(i, j) {
      if (this.maze[i][j] !== null) {
        this.maze[i][j].pro = parseInt(this.maze[i][j].pro)
      }
    },
    solve() {
      this.canGetSolve = false
      this.getSolve()
    },
    getSolve() {
      this.finalSudoku = []
      for (let index = 0; index < this.sudoku.length; index++) {
        this.finalSudoku.push(this.theAnswer[index])
      }
      this.initialMaze()
    },
    onSubmit() {
      this.canGetSolve = true
      this.createSudoku()
    },
    createSudoku() {
      this.initialAll()
      this.generateSudoku()
      this.setNull()
      this.initialMaze()
    },
    saveTheAnswer() {
      this.theAnswer = JSON.parse(JSON.stringify(this.sudoku))
    },
    judgeColor(i, j) {
      return this.judgePosition(i, j) + this.judgeNumber(i, j)
    },
    judgePosition(i, j) {
      if ((i < 3 || i > 5) && (j < 3 || j > 5) || (i > 2 && i < 6 && j > 2 && j < 6)) {
        return 'input1'
      } else {
        return 'input2'
      }
    },
    judgeNumber(i, j) {
      let style = ''
      let pro = this.maze[i][j]
      if (!pro.origin && (this.existSameProInRow(pro) || this.existSameProInCol(pro) || this.existSameProInGroup(pro))) {
        style = 'animation'
      }
      return style
    },
    existSameProInRow(x) {
      for (let index = 0; index < 9; index++) {
        if (x.pro !== null && parseInt(x.pro) === this.maze[x.row][index].pro && index !== x.col) {
          return true
        }
      }
      return false
    },
    existSameProInCol(x) {
      for (let index = 0; index < 9; index++) {
        if (x.pro !== null && parseInt(x.pro) === this.maze[index][x.col].pro && index !== x.row) {
          return true
        }
      }
      return false
    },
    existSameProInGroup(x) {
      let minRow = parseInt(x.row / 3)
      let maxRow = minRow + 2
      let minCol = parseInt(x.col / 3)
      let maxCol = minCol + 2
      for (let row = minRow; row < maxRow; row++) {
        for (let col = minCol; col < maxCol; col++) {
          if (x.pro !== null && parseInt(x.pro) === this.maze[row][col] && (row !== x.row || col !== x.col)) {
            return true
          }
        }
      }
      return false
    },
    getBackOfMaze() {
      this.initialMaze()
    },
    initialMaze() {
      this.maze = new Array(9);
      for (let i = 0; i < 9; i++) {
        this.maze[i] = new Array(9);
        for (let j = 0; j < 9; j++) {
          for (let index = 0; index < this.finalSudoku.length; index++) {
            let x = this.finalSudoku[index]
            if (x.row === i && x.col === j) {
              if (x.pro === null) {
                x.origin = false
              } else {
                x.origin = true
              }
              this.maze[i][j] = x
            }
          }
        }
      }
    },
    // 打乱列表顺序
    disorder(list) {
      let length = list.length
      for (let index = 0; index < length - 1; index++) {
        let random = parseInt(Math.random() * (length - 1 - index) + index + 1, 10);
        let x = list[index]
        list[index] = list[random]
        list[random] = x
      }
      return list
    },
    // 生成数据
    generateSudoku() {
      this.generateFirstRow()
      this.generate(this.encode(1, 0))
    },
    // 生成第一行
    generateFirstRow() {
      let array = new Array(1, 2, 3, 4, 5, 6, 7, 8, 9)
      array = this.disorder(array)
      for (let col = 0; col < array.length; col++) {
        this.sudoku.push({row: 0, col: col, pro: array[col]})
      }
    },
    generate(code) {
      if (this.sudoku.length === 81) {
        this.saveTheAnswer()
        this.setFinalSudoku()
        return
      }
      let array = this.getOptions(this.decodeRow(code), this.decodeCol(code))
      if (array.length === 0) {
        return
      }
      for (let x of array) {
        this.sudoku.push({row: this.decodeRow(code), col: this.decodeCol(code), pro: x})
        this.generate(this.getNextCode(code))
        if (this.sudoku.length === 81) {
          return
        }
        this.sudoku.pop()
      }
    },
    setFinalSudoku() {
      for (let index = 0; index < this.sudoku.length; index++) {
        this.finalSudoku.push(this.sudoku[index])
      }
    },
    getNextCode(code) {
      let row = parseInt(this.decodeRow(code))
      let col = parseInt(this.decodeCol(code))
      if (col < 8) {
        col = col + 1
      } else if (row < 8) {
        row = row + 1
        col = 0
      }
      return this.encode(row, col)
    },
    getOptions(row, col) {
      let allSet = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9])
      allSet = this.removeSameRow(row, allSet)
      allSet = this.removeSameCol(col, allSet)
      allSet = this.removeSamegroup(row, col, allSet)
      return this.disorder(allSet)
    },
    removeSameRow(row, allSet) {
      let rowSet = new Set()
      this.sudoku.forEach(element => {
        if (element.row === row) {
          rowSet.add(element.pro)
        }
      });
      return this.difference(allSet, rowSet)
    },
    difference(setA, setB) {
      let _difference = new Set(setA);
      for (let elem of setB) {
        _difference.delete(elem);
      }
      return _difference;
    },
    removeSameCol(col, allSet) {
      let colSet = new Set()
      this.sudoku.forEach(element => {
        if (element.col === col) {
          colSet.add(element.pro)
        }
      });
      return this.difference(allSet, colSet)
    },
    removeSamegroup(row, col, allSet) {
      let groupSet = new Set()
      this.sudoku.forEach(element => {
        if (parseInt(element.col / 3) === parseInt(col / 3) && parseInt(element.row / 3) === parseInt(row / 3)) {
          groupSet.add(element.pro)
        }
      });
      return this.difference(allSet, groupSet)
    },
    encode(row, col) {
      row = parseInt(row)
      col = parseInt(col)
      return row * 100 + col
    },
    decodeRow(code) {
      return parseInt(code / 100)
    },
    decodeCol(code) {
      return parseInt(parseInt(code) - parseInt(code / 100) * 100)
    },
    // 挖数据（随机挖，然后用解数独方法验证难度）
    // 每个宫均随机保留三个数，每次一个宫，然后判断是否有唯一解，如果没有唯一解就重新随机。
    setNull() {
      let max = 8
      let min = 0
      let array = new Array()
      let start = this.nullArray.length
      for (let index = 0; index < 6; index++) {
        array.push(this.getRandom(min, max))
      }
      if (typeof (this.nullArray) === 'undefined') {
        this.nullArray = new Array()
      }
      let copy = this.nullArray
      let finalSudokuCopy = this.finalSudoku
      this.nullArray.push(...array)
      let end = this.nullArray.length
      for (let i = 0; i < end - start; i++) {
        let code = this.getCodeByGroupAndIndex(array[i])
        this.setNullByCode(code)
      }
      let pass = this.checkNullArray()
      if (pass && this.count < 8) {
        this.count++
        this.setNull()
      } else if (!pass && this.count < 8) {
        this.count--
        this.finalSudoku = this.finalSudokuCopy
        this.nullArray = copy
        this.setNull()
      } else {
        return
      }
    },
    setNullByCode(code) {
      let row = this.decodeRow(code)
      let col = this.decodeCol(code)
      this.finalSudoku.forEach(x => {
        if (x.row === row && x.col === col) {
          x.pro = null
        }
      })
    },
    // 检查数独是否有唯一解（即找出数独全部解）
    checkNullArray() {
      this.getMaybeMap()
      this.allAnswer = []
      this.getAllAnswer()
      if (this.allAnswer.length > 1) {
        return false
      } else {
        return true
      }
    },
    getAllAnswer() {
      let copy = this.answer
      this.getAnswerByIterator(copy, this.getFirstKeyFromMaybeMap())
    },
    proExist(track, point) {
      let exist = false
      for (let x of track) {
        if ((x.row === point.row || x.col === point.col || parseInt(x.row / 3) === parseInt(point.row / 3) && parseInt(x.col / 3) === parseInt(point.col / 3)) && x.pro === point.pro) {
          exist = true
        }
      }
      return exist
    },
    getAnswerByIterator(track, code) {
      if (code !== null) {
        if (track.length === 81) {
          this.allAnswer.push(track)
          return
        }
        let options = this.maybeMap.get(code).filter(x => !this.proExist(track, x))
        if (options.length === 0) {
          return
        }
        for (let x of options) {
          track.push(x)
          this.getAnswerByIterator(track, this.getNextKeyFromMaybeMap(code))
          track.pop()
        }
      }
    },
    buildPoints(x) {
      let existSet = new Set()
      this.finalSudoku.forEach(element => {
        if (element.row === x.row) {
          existSet.add(element.pro)
        }
        if (element.col === x.col) {
          existSet.add(element.pro)
        }
        if (parseInt(element.row / 3) === parseInt(x.row / 3) && parseInt(element.col / 3) === parseInt(x.col / 3)) {
          existSet.add(element.pro)
        }
      })
      let resultSet = this.difference(new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]), existSet)
      let result = []
      resultSet.forEach(element => {
        result.push({row: x.row, col: x.col, pro: element})
      })
      return result
    },
    getMaybeMap() {
      for (let x of this.finalSudoku) {
        if (x.pro !== null) {
          this.answer = new Array()
          this.answer.push(x)
        } else {
          if (typeof (this.maybeMap) === 'undefined') {
            this.maybeMap = new Map()
          }
          this.maybeMap.set(this.encode(x.row, x.col), this.buildPoints(x))
        }
      }
    },
    getFirstKeyFromMaybeMap() {
      let array = new Array()
      for (let [key, value] of this.maybeMap) {
        array.push(key)
      }
      array.sort((a, b) => {
        return parseInt(a) - parseInt(b)
      })
      return array[0]
    },
    getNextKeyFromMaybeMap(code) {
      let array = new Array()
      for (let [key, value] of this.maybeMap) {
        array.push(key)
      }
      array.sort((a, b) => {
        return parseInt(a) - parseInt(b)
      })
      let index = array.indexOf(code)
      if (index !== array.length - 1) {
        return array[index + 1]
      } else {
        return null
      }
    },
    getCodeByGroupAndIndex(index) {
      let x = this.count.toString(3)
      x = parseInt(x)
      let groupRow = parseInt(x / 10)
      let groupCol = x - groupRow * 10

      let RowIndexSet = []
      let ColIndexSet = []
      let coList = []

      for (let i = 0; i < 9; i++) {
        if (parseInt(i / 3) === groupRow) {
          RowIndexSet.push(i)
        }
        if (parseInt(i / 3) === groupCol) {
          ColIndexSet.push(i)
        }
      }

      for (let rowIndex of RowIndexSet) {
        for (let colIndex of ColIndexSet) {
          coList.push([rowIndex, colIndex])
        }
      }

      let point = coList[index]
      return this.encode(point[0], point[1])

    },
    getRandom(min, max) {
      return Math.floor(Math.random() * (max - min + 1) + min)
    },
    initialAll() {
      this.sudoku = new Array()
      this.finalSudoku = new Array()
      for (let index = 0; index < 9; index++) {
        this.maze[index] = new Array();
        for (let indexJ = 0; indexJ < 8; indexJ++) {
          this.maze[index][indexJ] = 0;
        }
      }
      this.nullArray = new Array()
      this.allAnswer = new Array()
      this.theAnswer = new Array()
      this.maybeMap = new Map(),
        this.answer = new Array(),
        this.count = 0,
        this.group = 0
    }
  },
  mounted() {
  }
}
</script>

<style lang="scss" scoped>
.loading-mask {
  position: relative;
}

.scroll-expand {
  padding-right: 0;
  overflow: initial !important;

  .el-scrollbar__wrap {
    overflow: initial !important;
    overflow-x: hidden;

    .el-scrollbar__view {
      height: 100%;
    }
  }
}

.scroll-collapse {
  padding-right: 15px;

  .el-scrollbar__wrap {
    overflow: inherit !important;

    .el-scrollbar__view {
      height: 100%;
    }
  }

  overflow: initial !important;
}


/* 定义keyframe动画，命名为blink */
@keyframes blink {
  0% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* 添加兼容性前缀 */
@-webkit-keyframes blink {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@-moz-keyframes blink {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@-ms-keyframes blink {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@-o-keyframes blink {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

.input1 {
  outline-style: none;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 10px 10px;
  width: 620px;
  font-size: 24px;
  background-color: rgb(224, 194, 94);
}

.input2 {
  outline-style: none;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 10px 10px;
  width: 620px;
  font-size: 24px;
  background-color: rgb(224, 19, 80);
}

.input1animation {
  outline-style: none;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 10px 10px;
  width: 620px;
  font-size: 24px;
  background-color: rgb(224, 194, 94);
  animation: blink 1s linear infinite;
  /* 其它浏览器兼容性前缀 */
  -webkit-animation: blink 1s linear infinite;
  -moz-animation: blink 1s linear infinite;
  -ms-animation: blink 1s linear infinite;
  -o-animation: blink 1s linear infinite;
}

.input2animation {
  outline-style: none;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 10px 10px;
  width: 620px;
  font-size: 24px;
  background-color: rgb(224, 19, 80);
  animation: blink 1s linear infinite;
  /* 其它浏览器兼容性前缀 */
  -webkit-animation: blink 1s linear infinite;
  -moz-animation: blink 1s linear infinite;
  -ms-animation: blink 1s linear infinite;
  -o-animation: blink 1s linear infinite;

}
</style>
